﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using LightPersist;
using LightPersist.DomainGenerator;
using LightPersist.Metainfo;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

[LoadInSeparateAppDomainAttribute]
public class LightPersistModelGenerator : AppDomainIsolatedTask
{
	static PropertyMetainfo _lastProperty; // TODO make it better
	static ClassMetainfo _lastClass; // TODO make it better
	protected readonly List<ITaskItem> IncludeFileInBuild = new List<ITaskItem>();
	//readonly TaskLoggingHelper _log;

	public LightPersistModelGenerator()
	{
		//_log = new TaskLoggingHelper(this);
	}

	[Output]
	public ITaskItem[] Include
	{
		get { return IncludeFileInBuild.ToArray(); }
	}

	[Required]
	public ITaskItem[] Sources { get; set; }

	[Required]
	public string IntermediateOutputPath { get; set; }

	[Required]
	public string ProjectDir { get; set; }

	[Required]
	public string TargetName { get; set; }

	public string Debug { get; set; }

	public override bool Execute()
	{
		MessageDebug("Execute");
		if (!string.IsNullOrEmpty(Debug))
		{
			MessageDebug("Debug = " + Debug);
			while (!Debugger.IsAttached)
			{
				System.Threading.Thread.Sleep(300);
			}
		}
		var sources = Sources.ToArray();
		MessageDebug("Sources Count = " + sources.Length);
		foreach (var file in Sources)
		{
			if (file.ItemSpec.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
			{
				MessageDebug("Generating for " + file.ItemSpec);
				if (!GenerateFor(file))
				{
					return false;
				}
				MessageDebug("Success");
			}
			else
			{
				MessageDebug("Source skipped " + file.ItemSpec + " does not ends with .xaml");
			}
		}
		if (sources.Length <= 0)
		{
			Message("Nothing to process");
		}
		return true;
	}

	bool GenerateFor(ITaskItem item)
	{
		var sourceFile = item.ItemSpec;

		Message("Generate Code For: " + sourceFile);

		var fullObj = IntermediateOutputPath;
		if (!Path.IsPathRooted(fullObj))
		{
			fullObj = Path.Combine(ProjectDir, IntermediateOutputPath);
		}
		var generatedFileName = Path.Combine(fullObj, sourceFile + ".g.cs");

		// Generate only when code is not yet generated or it is out of dete comparing with the source xaml or Generator assembly (in cases generating algorithm are changed)
		if (!File.Exists(generatedFileName) || File.GetLastWriteTimeUtc(generatedFileName) < File.GetLastWriteTimeUtc(sourceFile) || File.GetLastWriteTimeUtc(generatedFileName) < File.GetLastWriteTimeUtc(typeof(LightPersistModelGenerator).Assembly.Location))
		{
			var code = GenerateCode(File.ReadAllText(sourceFile), sourceFile);
			Message("WRITE TO: " + generatedFileName);
			// Message("CODE: " + code);
			var dir = Path.GetDirectoryName(generatedFileName);
			if (!Directory.Exists(dir))
			{
				Directory.CreateDirectory(dir);
			}
			File.WriteAllText(generatedFileName, code);
		}
		else
		{
			Message("Generation is up to date.");
		}

		IncludeFileInBuild.Add(new TaskItem(generatedFileName));
		return true;
	}

	public string GenerateCode(string domainDeclarations, string fileName)
	{
		var code = new StringBuilder();
		const string headerFormat = @"
// THIS FILE ARE GENERATED AUTOMATICALLY
// DO NOT MODIFY

{0}

";
		var domain = (DomainMetainfo)XamlReader.Parse(domainDeclarations);
		Prepare(domain);

		code.AppendFormat(@"namespace {0}
{{
", domain.DomainName);

		var domainNameParts = domain.DomainName.Split('.');
		var domainNameLast = domainNameParts[domainNameParts.Length - 1];
		var domainFactoryClassName = Constants.GeneratedDomainMetainfoFactoryClassFormat.ArgI(domainNameLast);
		var domainFactoryPropertyName = Constants.GeneratedDomainMetainfoFactoryPropertyName.ArgI(domainNameLast);
		code.AppendLine((@"
	public class {0} : IDomainMetainfoProvider
	{{
		public DomainMetainfo Meta
		{{
			get
			{{
				return Metainfo;
			}}
		}}

		static {0}()
		{{
			{3}
		}}

		private static DomainMetainfo _domainMetainfo;
		public static DomainMetainfo {1}
		{{
			get
			{{
				if (_domainMetainfo == null)
				{{
{4}
					_domainMetainfo = Construct();
				}}
				return _domainMetainfo;
			}}
		}}

		private static DomainMetainfo Construct()
		{{
// LightProfiling.Lap(""MetainfoConstructing..."");
{2}
//LightProfiling.Lap(""MetainfoConstructed"");
		}}
	}}
").ArgI(domainFactoryClassName, // {0}
			domainFactoryPropertyName, // {1}
			ConstructionCodeSerializer.GetConstuctionCode(domain), // {2}
			GetFactoryRegisters(domain), // {3}
#if DEBUG
			@"
#if DEBUG && !PocketPC
var file = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(typeof({2}).Assembly.Location), @""{0}"");
if (System.IO.File.Exists(file))
{{
	throw new LightPersist.Exceptions.LightPersistException(string.Format(@""
Do not copy domain xaml to the target dirrectory!
File {{0}} is exists inside target dir.
Please, remove it and disable copying.
Now domain xaml is compiled-in.
You can use {1} to get DomainMetaInfo
"", file));
}}
#endif
".ArgI(fileName, Constants.GeneratedDomainMetainfoFactoryPropertySample, domainFactoryClassName)
#else
	null
#endif
			));

		foreach (var classMeta in domain.Classes)
		{
			_lastClass = classMeta;
			var classShortName = DomainMetainfo.GetShortName(classMeta.Name);

			string parent = null;
			if(classMeta.ParentClass!=null)
			{
				parent = " : I" + DomainMetainfo.GetShortName(classMeta.ParentClass);
			}

//			code.AppendFormat(@"	public interface I{0}{1}
//	{{
//		Guid ID {{ get; }}
//
//", classShortName, parent);
//
//			foreach (var propMeta in classMeta.PropertiesDeclared)
//			{
//				GenerateCodeForProperty(code, domain, propMeta, true);
//			}
//			code.AppendFormat(@"	}}
//
//"); // end iface
			code.AppendFormat(@"	public partial class {0} : {1}
	{{

		public {0}(LightPersistContext ctx) : base(ctx)
		{{
			{0}Init();
		}}

		partial void {0}Init();

", classShortName, DomainMetainfo.GetShortName(classMeta.ParentClass ?? "ModelBase"));

			foreach (var propMeta in classMeta.PropertiesDeclared)
			{
				GenerateCodeForProperty(code, domain, propMeta, false);
			}
			code.AppendFormat(@"	}}

"); // end class
		}
		code.AppendLine(@"}"); // end namespace

		code.Insert(0, headerFormat.ArgI(string.Join("", domain.GetUsings().Where(x => x.Length > 0).Select(x => "using " + x + ";" + Environment.NewLine).ToArray())));

		return code.ToString();
	}

	static string GetFactoryRegisters(DomainMetainfo domain)
	{
		return string.Join(@"
			", domain.Classes.Select(x => "LightPersistContext.RegisterFactory<" + x.TypeNameFull + ">(ctx => new " + x.TypeNameFull + "(ctx));").ToArray());
	}

//	private static string GetPreparedXamlModel(DomainMetainfo domain)
//	{
//		var settings = new XmlWriterSettings
//		               	{
//		               		Indent = true,
//		               		IndentChars = "\t",
//		               		NewLineOnAttributes = true,
//		               		ConformanceLevel = ConformanceLevel.Fragment
//		               	};
//		var sb = new StringBuilder();
//		var writer = XmlWriter.Create(sb, settings);
//		var manager = new XamlDesignerSerializationManager(writer)
//		              	{
//		              		XamlWriterMode = XamlWriterMode.Expression
//		              	};
//		XamlWriter.Save(domain, manager);
//		return 
//			//DomainMetainfo.ToBase64
//			(sb.ToString())
//			.Replace('"','\'')
//			;
//	}

	void Prepare(DomainMetainfo domain)
	{
		domain.AssemblyName = TargetName;
		domain.Prepare(true);
		domain.PreparedAtGenerator = true;
	}

	void GenerateCodeForProperty(StringBuilder code, DomainMetainfo domain, PropertyMetainfo propMeta, bool isIface)
	{
		Validate(propMeta);

		#region Xml Comment

		var xmlComment = new StringBuilder();

		if (propMeta.RefType == ReferenceType.Weak)
		{
			xmlComment.AppendLine("\t\t/// This property stores weak references");
		}

		if (propMeta.IsInverseOf)
		{
			xmlComment.AppendLine("\t\t/// Inverse property for \"{0}.{1}\"".ArgI(DomainMetainfo.GetClassFromAqn(propMeta.Type), propMeta.InverseOf));
		}

		if (xmlComment.Length > 0)
		{
			code.AppendFormat(@"		/// <summary>
{0}		/// </summary>
", xmlComment);
		}

		#endregion

		var propertyElementType = GetTypeFor(propMeta);

		var propertyTypeName = propMeta.IsList ? "ILightListProxy<{0}>".Arg(propertyElementType) : propertyElementType;

		string function;
		//string cast = null;
		if (propMeta.IsList)
		{
			function = "List".Arg(propertyElementType);
		}
		else
		{
			function = "Value";
			//cast = "({0})".Arg(propertyTypeName);
		}

		var needSetter = !propMeta.IsList && !propMeta.IsReadonly;

		var param = new[] { propMeta.Name, propertyTypeName, propertyElementType, function };

		code.AppendLine(@"		public {1} {0}
		{{
			get {{ return Get{3}<{2}>(""{0}""); }}".ArgI(param));
		if (needSetter)
		{
			code.AppendLine(@"			set {{ Set{3}<{2}>(value, ""{0}""); }}".ArgI(param));
		}
		code.AppendLine(@"		}
");
	}



	static void Validate(PropertyMetainfo p)
	{
		_lastProperty = p;
		// Should(!string.IsNullOrEmpty(p.Type), "Specify 'Type' attribute of property metainfo");
		// Should(!string.IsNullOrEmpty(p.Name), "Specify 'Name' attribute of property metainfo");

		switch (p.RefType)
		{
			case ReferenceType.Strong:
				break;
			case ReferenceType.Weak:
				break;
			default:
				Should(false, "RefType is unknown");
				break;
		}

		if (p.IsList)
		{
			switch (p.UntypedBehaviour)
			{
				case UntypedPropertyBehaviour.Unknown:
				case UntypedPropertyBehaviour.AllowModel:
					break;
				case UntypedPropertyBehaviour.PrimitiveOnly:
					Should(false, "UntypedBehaviour=PrimitiveOnly will not take effect on lists, even for untyped element typeName. Lists are allways model entity, because list changes must be tracked. Remove this property.");
					break;
				default:
					Should(false, "UntypedBehaviour is unknown: " + p.UntypedBehaviour);
					break;
			}
		}
		else
		{
			if (p.IsReadonly)
			{
				Should(p.IsInverseOf, "Consider use readonly with inverse-of properties. If you need break this rule, consult with Light Persist developers about removing it.");
			}
		}
	}

	static void Should(bool cond, string message)
	{
		if (!cond)
		{
			throw new ApplicationException("\r\n\r\nDomain Compile Time Validation:\r\n\r\n" + message + string.Format("\r\n\r\nPropertyName: \t{0}\r\nPropertyType: \t{1}\r\nClass: \t{2}\r\n\r\n", _lastProperty.Name, _lastProperty.Type, _lastClass.Name));
		}
	}

	static string GetTypeFor(PropertyMetainfo property)
	{
//		var declared = property.DeclaringClass.DeclaringDomain.FindClass(property.Type, false);

		var type = property.Type;
		// cut off assembly qualified name: [System.Int32, mscore] => [System.Int32]
		var i = type.IndexOf(',');
		var name = i == -1 ? type : type.Substring(0, i);
/*
		if (iface && declared != null)
		{
			var parts = name.Split('.');
			parts[parts.Length - 1] = "I" + parts[parts.Length - 1];
			name = string.Join(".", parts);
		}
*/
		return name;
	}

	static string GetTypeFor(ClassMetainfo clazz)
	{
//		var declared = clazz.DeclaringDomain.FindClass(clazz.Name);

		var type = clazz.Name;
		// cut off assembly qualified name: [System.Int32, mscore] => [System.Int32]
		var i = type.IndexOf(',');
		var name = i == -1 ? type : type.Substring(0, i);
/*
		if (iface && declared != null)
		{
			var parts = name.Split('.');
			parts[parts.Length - 1] = "I" + parts[parts.Length - 1];
			name = string.Join(".", parts);
		}
*/
		return name;
	}

	void Message(string msg)
	{
		Log.LogMessage(MessageImportance.High, "* Task LP: " + msg);
	}

	[Conditional("DEBUG")]
	void MessageDebug(string msg)
	{
		Log.LogMessage(MessageImportance.High, "* Task LP: Debug: " + msg);
	}
}